package integermap.main;

import java.util.Arrays;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 * Map for storing pairs [int]=>[int] with good performance and low memory usage
 * Uses int arrays for storing pairs with open addressing
 * @author alex
 */
public class IntegerHashMap implements IntegerMap {
    private int[] keys;
    private int[] values;

    private int size;
    private int capacity;
    private float loadFactor;

    private static final int DEFAULT_CAPACITY = 16;
    private static final float DEFAULT_LOAD_FACTOR = 0.9f;
    private static final float RELOAD_RATE = 1.1f;

    /**
     * The default constructor
     */
    public IntegerHashMap() {
        this(IntegerHashMap.DEFAULT_CAPACITY, IntegerHashMap.DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructor with parameter capacity
     * @param capacity the capacity
     */
    public IntegerHashMap(int capacity) {
        this(capacity, IntegerHashMap.DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructor with parameters capacity and load factor
     * @param capacity the capacity
     * @param loadFactor the load factor
     */
    public IntegerHashMap(int capacity, float loadFactor) {
        this.capacity = capacity;
        this.loadFactor = loadFactor;

        this.keys = initArray(new int[this.capacity]);
        this.values = initArray(new int[this.capacity]);
    }

    /**
     * Put element with provided key
     * @param key the key
     * @param value the value of element
     * @return the value of old element
     * @throws IllegalArgumentException
     */
    @Override
    public Integer put(Integer key, Integer value) {
        if (key == Integer.MIN_VALUE)
            throw new IllegalArgumentException("IntegerHashMap key can not be primitive null");

        Integer oldValue = get(value);

        int index = findIndex(key, this.keys);
        if (this.keys[index] == Integer.MIN_VALUE) {
            this.keys[index] = key;
            this.size++;
        }
        this.values[index] = value;

        if (this.size >= this.capacity * this.loadFactor)
            reload();

        return oldValue;
    }

    /**
     * Get element with provided key
     * @param key the key
     * @return the value of element
     */
    @Override
    public Integer get(Integer key) {
        int index = findIndex(key, this.keys);
        if (this.keys[index] == key)
            return this.values[index];
        else
            return null;
    }

    /**
     * Clears all elements
     */
    @Override
    public void clear() {
        initArray(this.keys);
        initArray(this.values);
        this.size = 0;
    }

    /**
     * Returns the count of elements
     * @return the count of elements
     */
    @Override
    public int size() {
        return this.size;
    }

    /**
     * Returns the set of keys
     * @return the set of keys
     */
    @Override
    public Set keySet() {
        Set<Integer> keySet = new HashSet<>();
        for (int key : this.keys) {
            if (key != Integer.MIN_VALUE)
                keySet.add(key);
        }
        return keySet;
    }

    /**
     * Returns index of element with provided hash
     * @param hash the hash
     * @param capacity the capacity
     * @return the element index
     */
    private int calculateIndex(int hash, int capacity) {
        return Math.abs(hash % capacity);
    }

    /**
     * Returns the index of element with provided key
     * @param key the key
     * @param keys the array of keys
     * @return the element index
     */
    private int findIndex(int key, int[] keys) {
        int index = calculateIndex(key, keys.length);
        int i = 0;
        while (keys[index] != key && keys[index] != Integer.MIN_VALUE && i < keys.length) {
            if (index != keys.length - 1)
                index++;
            else
                index = 0;
            i++;
        }
        if (keys[index] == key || keys[index] == Integer.MIN_VALUE)
            return index;
        else
            throw new NoSuchElementException("IntegerHashMap index can not be found");
    }

    /**
     * Increase the capacity and moves elements on new indexes
     */
    private void reload() {
        int newCapacity = (int) Math.ceil(this.capacity * IntegerHashMap.RELOAD_RATE);
        int[] newKeys = initArray(new int[newCapacity]);
        int[] newValues = initArray(new int[newCapacity]);

        int index, key;
        for (int i = 0; i < this.capacity; i++) {
            key = this.keys[i];
            if (key != Integer.MIN_VALUE) {
                index = findIndex(key, newKeys);
                newKeys[index] = key;
                newValues[index] = this.values[i];
            }
        }

        this.keys = newKeys;
        this.values = newValues;
        this.capacity = newCapacity;
    }

    /**
     * Replace all elements of array with integer primitive nulls
     * @param array the array
     * @return the array containing integer primitive nulls
     */
    private int[] initArray(int[] array) {
        Arrays.fill(array, Integer.MIN_VALUE);
        return array;
    }
}